package com.bluejamesbond.text;
/*
* Copyright 2015 Mathew Kurian
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* SpannableDocumentLayout.java
* @author Mathew Kurian
*
* From TextJustify-Android Library v2.0
* https://github.com/bluejamesbond/TextJustify-Android
*
* Please report any issues
* https://github.com/bluejamesbond/TextJustify-Android/issues
*
* Date: 1/27/15 3:35 AM
*/
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.Layout;
import android.text.Spannable;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.style.LeadingMarginSpan;
import com.bluejamesbond.text.style.DirectionSpan;
import com.bluejamesbond.text.style.TextAlignment;
import com.bluejamesbond.text.style.TextAlignmentSpan;
import junit.framework.Assert;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
public abstract class SpannableDocumentLayout extends IDocumentLayout {
private static final int TOKEN_START = 0;
private static final int TOKEN_END = 1;
private static final int TOKEN_X = 2;
private static final int TOKEN_Y = 3;
private static final int TOKEN_ASCENT = 4;
private static final int TOKEN_DESCENT = 5;
private static final int TOKEN_LINE = 6;
private static final int TOKEN_LENGTH = 7;
private TextPaint workPaint;
private LinkedList<LeadingMarginSpanDrawParameters> mLeadMarginSpanDrawEvents;
private int[] tokens;
public SpannableDocumentLayout(Context context, TextPaint paint) {
super(context, paint);
workPaint = new TextPaint(paint);
tokens = new int[0];
}
private static int pushToken(int[] tokens, int index, int start, int end, float x, float y,
float ascent, float descent, int line) {
Assert.assertTrue(index % TOKEN_LENGTH == 0);
tokens[index + TOKEN_START] = start;
tokens[index + TOKEN_END] = end;
tokens[index + TOKEN_X] = (int) x;
tokens[index + TOKEN_Y] = (int) y;
tokens[index + TOKEN_ASCENT] = (int) ascent;
tokens[index + TOKEN_DESCENT] = (int) descent;
tokens[index + TOKEN_LINE] = line;
return index + TOKEN_LENGTH;
}
private static int[] ammortizeArray(int[] array, int index) {
if (index >= array.length) {
int[] newArray = new int[array.length * 2];
Arrays.fill(newArray, Integer.MAX_VALUE);
System.arraycopy(array, 0, newArray, 0, array.length);
return newArray;
}
return array;
}
private static LinkedList<Integer> tokenize(CharSequence source,
int start,
int end) {
LinkedList<Integer> units = new LinkedList<>();
if (start >= end) {
return units;
}
boolean charSearch = source.charAt(start) == ' ';
for (int i = start; i < end; i++) {
// If the end add the word group
if (i + 1 == end) {
units.add(i + 1);
start = i + 1;
}
// Search for the start of non-space
else if (charSearch && source.charAt(i) != ' ') {
if ((i - start) > 0) {
units.add(i);
}
start = i;
charSearch = false;
}
// Search for the end of non-space
else if (!charSearch && source.charAt(i) == ' ') {
units.add(i);
start = i + 1; // Skip the space
charSearch = true;
}
}
return units;
}
/**
* Returns the length that the specified CharSequence would have if
* spaces and control characters were trimmed from the start and end,
* as by {@link String#trim}.
*/
protected int getTrimmedLength(CharSequence s, int start, int end) {
while (start < end && s.charAt(start) <= ' ') {
start++;
}
int endCpy = end;
while (endCpy > start && s.charAt(endCpy - 1) <= ' ') {
endCpy--;
}
return endCpy - start;
}
@SuppressWarnings("ConstantConditions")
@Override
public boolean onMeasure(IProgress<Float> progress, ICancel<Boolean> cancelled) {
boolean done = true;
float parentWidth = params.getParentWidth();
float boundWidth =
params.getParentWidth() - params.getInsetPaddingLeft() - params.getInsetPaddingRight();
mLeadMarginSpanDrawEvents = new LinkedList<>();
StaticLayout staticLayout = new StaticLayout(getText(), (TextPaint) getPaint(),
(int) boundWidth, Layout.Alignment.ALIGN_NORMAL, 1, 0, false);
int[] newTokens = new int[TOKEN_LENGTH * 1000];
LeadingMarginSpan[] activeLeadSpans = new LeadingMarginSpan[0];
HashMap<LeadingMarginSpan, Integer> leadSpans = new HashMap<>();
TextAlignment defAlign = params.textAlignment;
Spannable textCpy = (Spannable) this.text;
Paint.FontMetricsInt fmi = paint.getFontMetricsInt();
int maxTextIndex = textCpy.length() - 1;
int lines = staticLayout.getLineCount();
int enableLineBreak = 0;
int index = 0;
int lineNumber;
float x;
float y = params.insetPaddingTop;
float left = params.insetPaddingLeft;
float right = params.insetPaddingRight;
float lineHeightAdd = params.lineHeightMultiplier;
float lastAscent;
float lastDescent;
boolean isParaStart = true;
boolean isReverse = params.reverse;
for (lineNumber = 0; lineNumber < lines; lineNumber++) {
if (cancelled.isCancelled()) {
done = false;
break;
}
progress.onUpdate((float) lineNumber / (float) lines);
newTokens = ammortizeArray(newTokens, index);
int start = staticLayout.getLineStart(lineNumber);
int end = staticLayout.getLineEnd(lineNumber);
float realWidth = boundWidth;
if (params.debugging) {
Console.log(start + " => " + end + " :: " + " " + -staticLayout.getLineAscent(lineNumber)
+ " " + staticLayout.getLineDescent(lineNumber) + " " + textCpy.subSequence(start, end)
.toString());
}
// start == end => end of textCpy
if (start == end || lineNumber >= params.maxLines) {
break;
}
// Get textCpy alignment for the line
TextAlignmentSpan[] textAlignmentSpans =
textCpy.getSpans(start, end, TextAlignmentSpan.class);
TextAlignment lineTextAlignment = textAlignmentSpans.length == 0 ? defAlign :
textAlignmentSpans[0].getTextAlignment();
// Calculate components of line height
lastAscent = -staticLayout.getLineAscent(lineNumber);
lastDescent = staticLayout.getLineDescent(lineNumber) + lineHeightAdd;
// Handle reverse
DirectionSpan[] directionSpans = textCpy.getSpans(start, end, DirectionSpan.class);
isReverse = directionSpans.length > 0 ? directionSpans[0].isReverse() : params.reverse;
// Line is ONLY a <br/> or \n
if (start + 1 == end &&
(Character.getNumericValue(textCpy.charAt(start)) == -1 ||
textCpy.charAt(start) == '\n')) {
// Line break indicates a new paragraph
// is next
isParaStart = true;
// Use the line-height of the next line
if (lineNumber + 1 < lines) {
y += enableLineBreak * (-staticLayout.getLineAscent(lineNumber + 1) + staticLayout
.getLineDescent(lineNumber + 1));
}
// Don't ignore the next line breaks
enableLineBreak = 1;
continue;
} else {
// Ignore the next line break
enableLineBreak = 0;
}
x = lineTextAlignment == TextAlignment.RIGHT ? right : left;
y += lastAscent;
// Line CONTAINS a \n
boolean isParaEnd = end == maxTextIndex ||
textCpy.charAt(Math.min(end, maxTextIndex)) == '\n' ||
textCpy.charAt(end - 1) == '\n';
if (isParaEnd) {
enableLineBreak = 1;
}
// LeadingMarginSpan block
if (isParaStart) {
// Process LeadingMarginSpan
activeLeadSpans = textCpy.getSpans(start, end, LeadingMarginSpan.class);
// Set up all the spans
if (activeLeadSpans.length > 0) {
for (LeadingMarginSpan leadSpan : activeLeadSpans) {
if (!leadSpans.containsKey(leadSpan)) {
// Default margin is everything
int marginLineCount = -1;
if (leadSpan instanceof LeadingMarginSpan.LeadingMarginSpan2) {
LeadingMarginSpan.LeadingMarginSpan2 leadSpan2 =
((LeadingMarginSpan.LeadingMarginSpan2) leadSpan);
marginLineCount = leadSpan2.getLeadingMarginLineCount();
}
leadSpans.put(leadSpan, marginLineCount);
}
}
}
}
float totalMargin = 0.0f;
int top = (int) (y - lastAscent);
int baseline = (int) (y);
int bottom = (int) (y + lastDescent);
for (LeadingMarginSpan leadSpan : activeLeadSpans) {
// TOKEN_X based on alignment
float calcX = x;
// LineAlignment
int lineAlignmentVal = 1;
if (lineTextAlignment == TextAlignment.RIGHT) {
lineAlignmentVal = -1;
calcX = parentWidth - x;
}
// Get current line count
int spanLines = leadSpans.get(leadSpan);
// Update only if the valid next valid
if (spanLines > 0 || spanLines == -1) {
leadSpans.put(leadSpan, spanLines == -1 ? -1 : spanLines - 1);
mLeadMarginSpanDrawEvents
.add(new LeadingMarginSpanDrawParameters(leadSpan, (int) calcX,
lineAlignmentVal, top, baseline,
bottom, start, end, isParaStart));
// Is margin required?
totalMargin += leadSpan.getLeadingMargin(isParaStart);
}
}
x += totalMargin;
realWidth -= totalMargin;
// Disable/enable new paragraph
isParaStart = isParaEnd;
// TextAlignmentSpan block
if (isParaEnd && lineTextAlignment == TextAlignment.JUSTIFIED) {
lineTextAlignment = isReverse ? TextAlignment.RIGHT : TextAlignment.LEFT;
}
if (params.debugging) {
Console.log(String.format("Align: %s, X: %fpx, Y: %fpx, PWidth: %fpx",
lineTextAlignment, x, y, parentWidth));
}
switch (lineTextAlignment) {
case RIGHT: {
float lineWidth = Styled.measureText(paint, workPaint, textCpy, start, end, fmi);
index = pushToken(newTokens, index, start, end, parentWidth - x - lineWidth, y,
lastAscent, lastDescent, lineNumber);
y += lastDescent;
continue;
}
case CENTER: {
float lineWidth = Styled.measureText(paint, workPaint, textCpy, start, end, fmi);
index = pushToken(newTokens, index, start, end, x + (realWidth - lineWidth) / 2,
y, lastAscent, lastDescent, lineNumber);
y += lastDescent;
continue;
}
case LEFT: {
index = pushToken(newTokens, index, start, end, x, y, lastAscent, lastDescent, lineNumber);
y += lastDescent;
continue;
}
}
// FIXME: Space at the end of each line, possibly due to scrollbar offset
// LinkedList<Integer> tokenized = tokenize(textCpy, start, end - 1);
// FIXME: 2016/4/16 Issue #105 bug
LinkedList<Integer> tokenized = tokenize(textCpy, start, end);
// If one long word without any spaces
if (tokenized.size() == 1) {
int stop = tokenized.get(0);
// If not all space, process
// characters individually
if (getTrimmedLength(textCpy, start, stop) != 0) {
float[] textWidths = new float[stop - start];
float sum = 0.0f, textsOffset = 0.0f, offset;
int m = 0;
Styled.getTextWidths(paint, workPaint, textCpy, start,
stop, textWidths, fmi);
for (float tw : textWidths) {
sum += tw;
}
offset = (realWidth - sum) / (textWidths.length - 1);
for (int k = start; k < stop; k++) {
index = pushToken(newTokens, index, k, k + 1,
x + textsOffset + (offset * m), y, lastAscent, lastDescent, lineNumber);
newTokens = ammortizeArray(newTokens, index);
textsOffset += textWidths[m++];
}
}
}
// Handle multiple words
else {
int m = 1;
int indexOffset = 0;
int startIndex = index;
int reqSpaces = (tokenized.size() - 1) * TOKEN_LENGTH;
int rtlZero = 0;
float rtlRight = 0;
float rtlMul = 1;
float lineWidth = 0;
float offset;
if (isReverse) {
indexOffset = -2 * TOKEN_LENGTH;
rtlRight = parentWidth;
rtlMul = -1;
rtlZero = 1;
// reverse index
index += reqSpaces;
}
// more space
newTokens = ammortizeArray(newTokens, index + reqSpaces);
for (int stop : tokenized) {
float wordWidth = Styled.measureText(paint, workPaint, textCpy,
start, stop, fmi);
// add word
index = pushToken(newTokens, index, start, stop, rtlRight + rtlMul * (x + lineWidth + rtlZero * wordWidth), y, lastAscent,
lastDescent, lineNumber);
lineWidth += wordWidth;
start = stop + 1;
// based on if rtl
index += indexOffset;
}
if (isReverse) {
index = startIndex + reqSpaces + TOKEN_LENGTH;
}
offset =
(realWidth - lineWidth) / (float) (tokenized.size() - 1);
if (isReverse) {
for (int pos = index - TOKEN_LENGTH * 2; pos >= startIndex; pos -= TOKEN_LENGTH) {
newTokens[pos + TOKEN_X] =
(int) (((float) newTokens[pos + TOKEN_X]) - (offset * (float) m++));
}
} else {
for (int pos = startIndex + TOKEN_LENGTH; pos < index; pos += TOKEN_LENGTH) {
newTokens[pos + TOKEN_X] =
(int) (((float) newTokens[pos + TOKEN_X]) + (offset * (float) m++));
}
}
}
y += lastDescent;
}
lineCount = lineNumber;
tokens = newTokens;
params.changed = false;
textChange = !done;
measuredHeight = (int) (y - lineHeightAdd + params.insetPaddingBottom);
return done;
}
@Override
public void onDraw(Canvas canvas, int scrollTop, int scrollBottom) {
if (tokens.length < TOKEN_LENGTH) {
return;
}
Spannable textCpy = (Spannable) this.text;
int startIndex = getTokenForVertical(scrollTop, TokenPosition.START_OF_LINE);
int endIndex = getTokenForVertical(scrollBottom, TokenPosition.END_OF_LINE);
boolean defIsReverse = false;
for (LeadingMarginSpanDrawParameters parameters : mLeadMarginSpanDrawEvents) {
// FIXME sort by Y and break out of loop
int top = parameters.top - scrollTop;
int bottom = parameters.bottom - scrollTop;
if (bottom < 0 || top > scrollBottom) continue;
parameters.span.drawLeadingMargin(canvas, paint, parameters.x,
parameters.dir, top, parameters.baseline,
bottom, textCpy, parameters.start,
parameters.end, parameters.first, null);
}
int lastEndIndexY = tokens[endIndex + TOKEN_Y];
int diffEndIndexYCount = 1;
// FIXME Find next pos-y
for (int s = endIndex; diffEndIndexYCount > 0 && s < tokens.length; s += TOKEN_LENGTH) {
endIndex += TOKEN_LENGTH;
if (lastEndIndexY != tokens[s + TOKEN_Y]) {
diffEndIndexYCount--;
lastEndIndexY = tokens[s + TOKEN_Y];
}
}
for (int index = startIndex; index < endIndex; index += TOKEN_LENGTH) {
if (tokens[index + TOKEN_START] == Integer.MAX_VALUE) break;
DirectionSpan[] directionSpans = textCpy.getSpans(tokens[index + TOKEN_START], tokens[index + TOKEN_END], DirectionSpan.class);
Styled.drawText(canvas, textCpy, tokens[index + TOKEN_START],
tokens[index + TOKEN_END], Layout.DIR_LEFT_TO_RIGHT, directionSpans.length > 0 ? directionSpans[0].isReverse() : defIsReverse,
tokens[index + TOKEN_X], 0,
tokens[index + TOKEN_Y] - scrollTop, 0, paint, workPaint, false);
if (params.debugging) {
int lastColor = paint.getColor();
float lastStrokeWidth = paint.getStrokeWidth();
paint.setStrokeWidth(2);
paint.setColor(Color.GREEN);
canvas.drawLine(0, tokens[index + TOKEN_Y] - tokens[index + TOKEN_ASCENT] - scrollTop,
params.parentWidth, tokens[index + TOKEN_Y] - tokens[index + TOKEN_ASCENT] - scrollTop,
paint);
paint.setColor(Color.CYAN);
canvas.drawLine(0, tokens[index + TOKEN_Y] + tokens[index + TOKEN_DESCENT] - scrollTop,
params.parentWidth, tokens[index + TOKEN_Y] + tokens[index + TOKEN_DESCENT] - scrollTop,
paint);
paint.setColor(lastColor);
paint.setStrokeWidth(lastStrokeWidth);
}
}
}
@Override
public float getTokenAscent(int tokenIndex) {
return tokens[tokenIndex + TOKEN_ASCENT];
}
@Override
public float getTokenDescent(int tokenIndex) {
return tokens[tokenIndex + TOKEN_DESCENT];
}
@Override
public int getTokenForVertical(float y, TokenPosition position) {
int high = Math.max(0, tokens.length - 1);
int low = 0;
while (low + 1 < high) {
int mid = (high + low) / 2;
int midx = mid - (mid % TOKEN_LENGTH);
int fY = tokens[midx + TOKEN_Y];
if (fY > y) {
high = mid;
} else {
low = mid;
}
}
switch (position) {
default:
case START_OF_LINE: {
low -= low % TOKEN_LENGTH;
for (int s = low; s > 0 && tokens[s + TOKEN_Y] >= y; s -= TOKEN_LENGTH) {
low -= TOKEN_LENGTH;
}
return low;
}
case END_OF_LINE: {
high -= high % TOKEN_LENGTH;
for (int s = high; s + TOKEN_LENGTH < tokens.length && tokens[s + TOKEN_Y] <= y; s += TOKEN_LENGTH) {
high += TOKEN_LENGTH;
}
return high;
}
}
}
@Override
public int getLineForToken(int tokenIndex) {
return tokens[tokenIndex + TOKEN_LINE];
}
@Override
public int getTokenStart(int tokenIndex) {
return tokens[tokenIndex + TOKEN_START];
}
@Override
public int getTokenEnd(int tokenIndex) {
return tokens[tokenIndex + TOKEN_END];
}
@Override
public float getTokenTopAt(int tokenIndex) {
return tokens[tokenIndex + TOKEN_Y];
}
@Override
public CharSequence getTokenTextAt(int index) {
return text.subSequence(tokens[index + TOKEN_START], tokens[index + TOKEN_END]);
}
@Override
public boolean isTokenized() {
return tokens != null;
}
/**
* Class to handle onDrawLeadingSpanMargin
*/
private class LeadingMarginSpanDrawParameters {
public int x;
public int top;
public int baseline;
public int bottom;
public int dir;
public int start;
public int end;
public boolean first;
public LeadingMarginSpan span;
public LeadingMarginSpanDrawParameters(LeadingMarginSpan span,
int x,
int dir,
int top,
int baseline,
int bottom,
int start,
int end,
boolean first) {
this.span = span;
this.x = x;
this.dir = dir;
this.top = top;
this.baseline = baseline;
this.bottom = bottom;
this.start = start;
this.end = end;
this.first = first;
}
}
}